package jmine.tec.security.josso.gateway.manager;

import static jmine.tec.security.josso.JOSSOSecurityException.BUNDLE;
import static jmine.tec.security.josso.gateway.auth.SecurityManagerAuthScheme.SCHEME_NAME;
import static org.josso.gateway.signon.Constants.KEY_JOSSO_SECURITY_DOMAIN_NAME;

import java.util.ArrayList;
import java.util.List;

import javax.security.auth.Subject;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import jmine.tec.security.josso.JOSSOHelper;
import jmine.tec.security.josso.JOSSOSecurityException;
import jmine.tec.security.josso.gateway.auth.SecurityManagerAuthScheme;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.josso.Lookup;
import org.josso.auth.Credential;
import org.josso.auth.exceptions.SSOAuthenticationException;
import org.josso.auth.scheme.UsernamePasswordCredentialProvider;
import org.josso.gateway.MutableSSOContext;
import org.josso.gateway.SSOContext;
import org.josso.gateway.SSOException;
import org.josso.gateway.SSOGateway;
import org.josso.gateway.SSORequestImpl;
import org.josso.gateway.SSOWebConfiguration;
import org.josso.gateway.assertion.AuthenticationAssertion;
import org.josso.gateway.identity.exceptions.NoSuchDomainException;
import org.josso.gateway.session.SSOSession;
import org.josso.gateway.session.exceptions.NoSuchSessionException;

import bancosys.tec.exception.MessageHolder;
import bancosys.tec.security.SecurityException;
import bancosys.tec.security.SecurityService;
import bancosys.tec.security.impl.web.WebSecurityContext;
import bancosys.tec.security.impl.web.WebSecurityManager;

/**
 * Implementação de SecurityManager que deve ser utilizada pela aplicação Gateway.
 * 
 * @author lundberg
 */
public class JOSSOGatewaySecurityManager implements WebSecurityManager {

    private static final Log LOG = LogFactory.getLog(JOSSOGatewaySecurityManager.class);

    private SecurityService securityService;

    /**
     * {@inheritDoc}
     */
    public Subject login(String username, String password, WebSecurityContext context) throws SecurityException {
        HttpServletRequest request = context.getRequest();

        SSOGateway gateway = JOSSOHelper.getSSOGateway(request.getSession().getServletContext());
        MutableSSOContext ssoContext = this.createSSOContext(request, gateway);

        Credential[] credentials = this.createAuthenticationCredentials(gateway, username, password, context);
        try {
            AuthenticationAssertion authAssertion = gateway.assertIdentity(credentials, SCHEME_NAME);
            SSOSession session = gateway.findSession(authAssertion.getSSOSessionId());
            ssoContext.setCurrentSession(session);
            return session.getSubject();
        } catch (SSOAuthenticationException e) {
            LOG.warn("Falha de autenticação para usuário " + username, e);
            return null;
        } catch (SSOException e) {
            LOG.error(e.getMessage(), e);
            throw new JOSSOSecurityException(new MessageHolder(BUNDLE, "jmine.tec.security.josso.unexpected.exception.authenticating"), e);
        } catch (NoSuchSessionException e) {
            LOG.error(e.getMessage(), e);
            throw new JOSSOSecurityException(new MessageHolder(BUNDLE, "jmine.tec.security.josso.unexpected.session.not.found"), e);
        }
    }

    /**
     * Cria as credenciais para autenticação
     * 
     * @param gateway gateway
     * @param username username
     * @param password password
     * @param context context
     * @return Credentials
     * @throws SecurityException SecurityException
     */
    private Credential[] createAuthenticationCredentials(SSOGateway gateway, String username, String password, WebSecurityContext context)
            throws SecurityException {
        List<Credential> credentials = new ArrayList<Credential>();
        try {
            credentials.add(gateway.newCredential(SCHEME_NAME, UsernamePasswordCredentialProvider.USERNAME_CREDENTIAL_NAME, username));
            credentials.add(gateway.newCredential(SCHEME_NAME, UsernamePasswordCredentialProvider.PASSWORD_CREDENTIAL_NAME, password));
            credentials.add(gateway.newCredential(SCHEME_NAME, SecurityManagerAuthScheme.WEB_SECURITY_CONTEXT_CREDENTIAL_NAME, context));
        } catch (SSOAuthenticationException e) {
            LOG.error(e.getMessage(), e);
            throw new JOSSOSecurityException(
                    new MessageHolder(BUNDLE, "jmine.tec.security.josso.unexpected.exception.building.credentials"), e);
        }
        return credentials.toArray(new Credential[credentials.size()]);
    }

    /**
     * Prepara o contexto para login
     * 
     * @param request request
     * @param gateway gateway
     * @return SSO Context
     * @throws SecurityException SecurityException
     */
    private MutableSSOContext createSSOContext(HttpServletRequest request, SSOGateway gateway) throws SecurityException {
        try {
            MutableSSOContext ssoContext = (MutableSSOContext) gateway.prepareSSOContext(new SSORequestImpl(request));
            ssoContext.setUserLocation(request.getRemoteHost());
            request.getSession().setAttribute(KEY_JOSSO_SECURITY_DOMAIN_NAME, ssoContext.getSecurityDomain().getName());
            ssoContext.setScheme(SCHEME_NAME);
            return ssoContext;
        } catch (NoSuchDomainException e) {
            LOG.error(e.getMessage(), e);
            throw new JOSSOSecurityException(new MessageHolder(BUNDLE, "jmine.tec.security.josso.no.such.domain"), e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void storeSubject(WebSecurityContext context, Subject subject) {
        SSOContext ssoContext = SSOContext.getCurrent();
        SSOWebConfiguration cfg = this.lookupSSOWebConfiguration();
        String name = org.josso.gateway.Constants.JOSSO_SINGLE_SIGN_ON_COOKIE + "_" + ssoContext.getSecurityDomain().getName();
        if (cfg.isSessionTokenOnClient()) {
            LOG.debug("Storing SSO Session ID on client");
            Cookie ssoCookie =
                    JOSSOHelper.newJossoCookie(cfg, context.getRequest().getContextPath(), name, ssoContext.getSession().getId());
            context.getResponse().addCookie(ssoCookie);
        } else {
            LOG.debug("Storing SSO Session ID on server");
            HttpSession hsession = context.getRequest().getSession();
            hsession.setAttribute(name, ssoContext.getSession().getId());
        }
        context.getRequest().getSession().setAttribute(SESSION_KEY, subject);
    }

    /**
     * Busca a configuração de Single Sign-On
     * 
     * @return SSOWebConfiguration
     */
    private SSOWebConfiguration lookupSSOWebConfiguration() {
        SSOWebConfiguration cfg;
        try {
            cfg = Lookup.getInstance().lookupSSOWebConfiguration();
        } catch (Exception e) {
            throw new IllegalStateException(new MessageHolder(BUNDLE, "jmine.tec.security.josso.web.configuration.not.found").getMessage(),
                    e);
        }
        return cfg;
    }

    /**
     * {@inheritDoc}
     */
    public Subject loadSubject(WebSecurityContext context) throws SecurityException {
        if (context.getRequest().getSession(true).getAttribute(SESSION_KEY) != null) {
            return (Subject) context.getRequest().getSession().getAttribute(SESSION_KEY);
        } else {
            return this.attemptAutomaticLogin(context);
        }
    }

    /**
     * Em algumas implantações o usuário não faz login no sistema, utilizando algum tipo de SSO externo. Nesse caso é feita a tentativa de
     * login sem usuário ou senha, repassando a chamada para o SecurityManager específico que será chamado através do
     * SecurityManagerAuthScheme. Dessa forma o SSO externo é integrado automaticamente no SSO JOSSO da aplicação.
     * 
     * @param context context
     * @return Subject
     * @throws SecurityException SecurityException
     */
    private Subject attemptAutomaticLogin(WebSecurityContext context) throws SecurityException {
        Subject subject = null;
        try {
            subject = this.login(null, null, context);
        } catch (SecurityException e) {
            LOG.error("Erro em tentativa de login automático.");
        }
        if (subject != null) {
            this.storeSubject(context, subject);
        }
        return subject;
    }

    /**
     * {@inheritDoc}
     */
    public void logout(WebSecurityContext context) {
        this.ssoLogout(context);
        this.removeSubjectFromSession(context);
    }

    /**
     * Remove o security subject da session
     * 
     * @param context context
     */
    private void removeSubjectFromSession(WebSecurityContext context) {
        context.getRequest().getSession().removeAttribute(SESSION_KEY);
    }

    /**
     * Efetua o logout da infra de Single Sign-On
     * 
     * @param context context
     */
    private void ssoLogout(WebSecurityContext context) {
        HttpServletRequest request = context.getRequest();
        HttpServletResponse response = context.getResponse();
        try {
            SSOGateway gateway = JOSSOHelper.getSSOGateway(request.getSession().getServletContext());
            MutableSSOContext ssoContext = this.createSSOContext(request, gateway);
            String jossoSessionId = JOSSOHelper.getJossoSessionId(request);
            if (jossoSessionId == null) {
                LOG.warn("Requisição de logout não possui Josso Session ID.");
                return;
            }
            ssoContext.setCurrentSession(gateway.findSession(jossoSessionId));
            gateway.logout();
        } catch (SSOException e) {
            LOG.error("Não foi possível efetuar logout do SSO.", e);
        } catch (SecurityException e) {
            LOG.error("Falha ao criar contexto para execução de logout do SSO.", e);
        } catch (NoSuchSessionException e) {
            LOG.error("Sessão não encontrada para efetuar logout do SSO.", e);
        }
        JOSSOHelper.removeJossoSessionId(request, response);
    }

    /**
     * {@inheritDoc}
     */
    public SecurityService getSecurityService() {
        return this.securityService;
    }

    /**
     * @param securityService the securityService to set
     */
    public void setSecurityService(SecurityService securityService) {
        this.securityService = securityService;
    }
}
